package com.jerome.fs.sync;

import com.jerome.fs.constants.AppConstant;
import com.jerome.fs.http.HttpMethodEnum;
import com.jerome.fs.server.aggregation.ContextUtil;
import com.jerome.fs.util.ThreadPoolFactory;

import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.URL;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * Socket Long Connection Client
 *
 * @author Jerome
 * @date 2023/9/11
 */
public class SyncClient {
    private Socket socket;
    private ExecutorService pool;
    private URL url;
    /**
     * Reconnect time
     */
    private long reconnectTime = 30 * 1000;
    /**
     * Heartbeat timeout time
     */
    private long keepAliveTime = 15 * 1000;
    private long lastTime = System.currentTimeMillis();
    private String version = "1";
    private PrintWriter printWriter;
    /**
     * Specific logic implementation
     */
    private SyncClientHandler handler;

    /**
     * Create a socket connection
     *
     * @param urlStr     The URL string
     * @param handler    The handler for handling socket requests
     * @param methodEnum The HTTP method to use
     * @throws IOException if there is an I/O error
     */
    public SyncClient(String urlStr, SyncClientHandler handler, HttpMethodEnum methodEnum) throws IOException {
        this.handler = handler;
        url = new URL(urlStr);
        reconnect();
        pool = new ThreadPoolExecutor(2, 2,
                0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>(1), new ThreadPoolFactory("SocketKAClient"));
        // Object for receiving requests
        pool.submit(new ReceiveWatchDog());
        if (HttpMethodEnum.PINGPONG.equals(methodEnum)) {
            pool.submit(new KeepAliveWatchDog(HttpMethodEnum.PINGPONG));
        } else {
            pool.submit(new KeepAliveWatchDog(HttpMethodEnum.REGISTER));
        }
    }

    /**
     * Reconnect the socket connection
     *
     * @throws IOException if there is an I/O error
     */
    private synchronized void reconnect() throws IOException {
        if (ContextUtil.getAggregationConfig().getMasterAddress().startsWith(AppConstant.HTTP_PREFIX)) {
            socket = SocketFactory.getDefault().createSocket(url.getHost(), url.getPort() == -1 ? 80 : url.getPort());
        } else {
            socket = SSLSocketFactory.getDefault().createSocket(url.getHost(), 443);
        }
    }

    /**
     * Synchronize all data
     *
     * @throws IOException            if there is an I/O error
     * @throws ClassNotFoundException if the class is not found during deserialization
     */
    public synchronized void syncAll() throws IOException, ClassNotFoundException {
        request(HttpMethodEnum.SYNC, "1", version);
    }

    /**
     * Incremental synchronization
     *
     * @param offset The offset for incremental synchronization
     * @throws IOException            if there is an I/O error
     * @throws ClassNotFoundException if the class is not found during deserialization
     */
    public synchronized void incrementSync(int offset) throws IOException, ClassNotFoundException {
        request(HttpMethodEnum.INCREMENT_SYNC, "" + offset, version);
    }

    /**
     * Service registration
     *
     * @throws IOException            if there is an I/O error
     * @throws ClassNotFoundException if the class is not found during deserialization
     */
    public synchronized void register() throws IOException, ClassNotFoundException {
        request(HttpMethodEnum.REGISTER, ":" + ContextUtil.getAggregationConfig().getPort(), version);
    }

    /**
     * Get the registration list
     *
     * @throws IOException            if there is an I/O error
     * @throws ClassNotFoundException if the class is not found during deserialization
     */
    public synchronized void pingPong() throws IOException, ClassNotFoundException {
        request(HttpMethodEnum.PINGPONG, version, version);
    }

    /**
     * Send a request
     *
     * @param httpMethodEnum The HTTP method to use
     * @param param1         Parameter 1
     * @param param2         Parameter 2
     * @throws IOException            if there is an I/O error
     * @throws ClassNotFoundException if the class is not found during deserialization
     */
    private void request(HttpMethodEnum httpMethodEnum, String param1, String param2) throws IOException, ClassNotFoundException {
        printWriter = new PrintWriter(socket.getOutputStream());
        printWriter.println(String.format("%s %s %s", httpMethodEnum.getCode(), param1, param2));
        printWriter.println(String.format("Host: %s", url.getHost() + ":" + url.getPort()));
        printWriter.println();
        printWriter.flush();
    }

    /**
     * Receive response messages
     */
    private class ReceiveWatchDog implements Runnable {

        @Override
        public void run() {
            ObjectInputStream inputStream;

            while (true) {
                try {
                    if (socket.isClosed()) {
                        Thread.sleep(1000);
                        continue;
                    }
                    if (socket.getInputStream().available() > 0) {
                        // Read object data
                        lastTime = System.currentTimeMillis();
                        inputStream = new ObjectInputStream(socket.getInputStream());
                        SyncVo o = (SyncVo) inputStream.readObject();
                        // Execute different logic
                        handler.handle(o);
                    } else {
                        Thread.sleep(10);
                    }
                } catch (Exception e) {
                    // End the thread in case of an error
                    System.out.println("ReceiveWatchDog: " + e.getMessage());
                }
            }

        }
    }

    /**
     * Watchdog for keeping the connection alive
     */
    private class KeepAliveWatchDog implements Runnable {
        private HttpMethodEnum methodEnum;

        public KeepAliveWatchDog(HttpMethodEnum methodEnum) {
            this.methodEnum = methodEnum;
        }

        @Override
        public void run() {
            while (true) {
                try {
                    if (socket.isClosed()) {
                        reconnect();
                        Thread.sleep(reconnectTime);
                        lastTime = System.currentTimeMillis();
                    } else {
                        if (System.currentTimeMillis() - lastTime > keepAliveTime) {
                            socket.close();
                            continue;
                        }
                    }
                    switch (methodEnum) {
                        case REGISTER:
                            register();
                            break;
                        case PINGPONG:
                            pingPong();
                            break;
                        default:
                            break;
                    }
                    Thread.sleep(5000);
                } catch (Exception e) {
                    // End the thread in case of an error
                    System.out.println("KeepAliveWatchDog err: " + e.getMessage());
                    try {
                        socket.close();
                    } catch (IOException ioException) {
                        ioException.printStackTrace();
                    }
                }
            }

        }
    }
}
